﻿using Daemon.Common;
using Daemon.Common.Cache;
using Daemon.Data.Infrastructure.Auth;
using Daemon.Data.Substructure.Const;
using Daemon.Data.Substructure.Enums;
using Daemon.Data.Substructure.Helpers;
using Daemon.Middleware;
using Daemon.Model;
using Daemon.Repository.Contract;
using Daemon.Repository.EF;
using Daemon.Repository.EF.Executor;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authentication.Cookies;
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Primitives;
using Microsoft.IdentityModel.Tokens;
using System;
using System.Collections.Generic;
using System.IdentityModel.Tokens.Jwt;
using System.IO;
using System.Linq;
using System.Net.Mail;
using System.Net.Mime;
using System.Security.Claims;
using System.Text;
using System.Threading;
using System.Threading.Tasks;

namespace DaemonAPI.Helpers
{
	internal static class TokenHelper
	{
		private const string TokenIssuer = "self";
		private const string TokenKey = "Transfinder";
		private const string SupportTokenKey = "SupportToken";
		private const int TokenRetryPauseInSeconds = 5;

		internal static string CreateToken(string userName, string prefix, BaseAuthUser userEntity, string supportToken = "")
		{
			Claim[] claims = new Claim[0];
			if (MFAHelper.IsEnableEmailAuth(userEntity) && UseMultifactorAuth(prefix))
			{
				claims = new[] { new Claim("Amr", "pwd,mfa") };
			}

			return CreateTokenCore(userName, prefix, claims.Concat(BuildClaims(userEntity)).ToArray(), supportToken);
		}

		internal static bool UseMultifactorAuth(string prefix)
		{
			return SystemConst.ENABLE_MULTIFACTOR_PREFIXS.Any(x => x.Equals(prefix, StringComparison.CurrentCultureIgnoreCase));
		}

		internal static bool SendMFACodeWithEmail(string emailAddress, string code, string clientKey, string prefix)
		{
			List<Attachment> attachments = new List<Attachment>();
			string appLogoName,
				appLogo,
				logoHtml = @"<tr>
								<td>
									<img src='cid:{0}'>
								</td>
							</tr>";

			switch (prefix)
			{
				case SystemConst.SERVICEFINDER_PREFIX:
					appLogo = SystemConst.SERVICEFINDER_LOGO_BASE64;
					appLogoName = "ServicefinderLogo";
					break;
				default:
					{
						appLogo = SystemConst.ROUTEFINDER_LOGO_BASE64;
						appLogoName = "PLUSLogo";
						break;
					}
			}

			byte[] bytes = Convert.FromBase64String(appLogo);
			bool result = false;

			using (MemoryStream ms = new MemoryStream(bytes))
			{
				var logoAttachment = new Attachment(ms, $"{appLogoName}.jpg", MediaTypeNames.Image.Jpeg);
				logoAttachment.ContentId = $"{appLogoName}";
				logoAttachment.ContentDisposition.Inline = true;
				attachments.Add(logoAttachment);
				var availableExpireTime = SystemConst.SERVICEFINDER_PREFIX.Equals(prefix) ? 10 : 5;

				var body = $@"<table style='width:100%;text-align:center;'>
							{string.Format(logoHtml, appLogoName)}
							<tr>
								<td>
									<p style='font-weight: 700;font-size:18px'>Your Security Code is:</p>
								</td>
							</tr>
							<tr>
								<td>
									<h1>{code}</h1>
								</td>
							</tr>
							<tr>
								<td>
									<p style='font-size:18px'><i>Note: it will only be active for {availableExpireTime} minutes.</i></p>
								</td>
							</tr>
						</table>";
				result = SESUtil.SendEmail(emailAddress, "Login Security Code", body, true, clientKey, attachments);
			}

			return result;
		}

		private static string CreateTokenCore(string userName, string prefix, Claim[] extraClaims, string supportToken, bool isSaveToken = true)
		{
			var isSupport = SupportModeHelper.IsSupportModeUser(userName);
			var httpContextAccessor = ServiceLocator.Resolve<IHttpContextAccessor>();
			var configuration = ServiceLocator.Resolve<IConfiguration>();
			JwtSecurityTokenHandler tokenHandler = new JwtSecurityTokenHandler();
			SymmetricSecurityKey key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(configuration["Jwt:Secret"]));

			DateTime expiresAt = GetExpiration();

			var identity = new ClaimsIdentity(JwtBearerDefaults.AuthenticationScheme);

			IEnumerable<Claim> claims = new Claim[] {
				new Claim("Name", userName),
				new Claim("CreateTime", DateTime.Now.ToString()),
				new Claim("Prefix", prefix),
				new Claim(ClaimTypes.Expiration,expiresAt.ToString())
			};

			if (isSupport)
			{
				var supportClaims = new[] { new Claim(SupportTokenKey, supportToken) };
				claims = claims.Concat(supportClaims).ToArray();
			}

			identity.AddClaims(claims);
			if (extraClaims != null)
			{
				identity.AddClaims(extraClaims);
			}

			httpContextAccessor.HttpContext.SignInAsync(JwtBearerDefaults.AuthenticationScheme, new ClaimsPrincipal(identity));

			var tokenDescriptor = new SecurityTokenDescriptor
			{
				Subject = identity,
				Issuer = configuration["Jwt:Issuer"],
				Audience = configuration["Jwt:Audience"],
				SigningCredentials = new SigningCredentials(key, SecurityAlgorithms.HmacSha256)
			};

			var token = tokenHandler.CreateJwtSecurityToken(tokenDescriptor);
			var tokenString = tokenHandler.WriteToken(token);
			var tokenKey = GetTokenKey(userName, prefix);
			var tokenInfo = new TokenInfo() { TokenKey = tokenKey, TokenValue = tokenString, Expiration = expiresAt };
			if (isSaveToken)
			{
				SaveToken(isSupport, tokenInfo);
			}

			return tokenString;
		}

		private static void SaveToken(bool isSupport, TokenInfo tokenInfo)
		{
			var tokenRepository = ServiceLocator.Resolve<ITokenInfoRepository>();
			if (isSupport)
			{
				var supportToken = GetClaimValueFromToken(tokenInfo.TokenValue, SupportTokenKey);
				var existTokenInfo = tokenRepository.FindTokenInfoListByKey(tokenInfo.TokenKey)
					.FirstOrDefault(r => GetClaimValueFromToken(r.TokenValue, SupportTokenKey) == supportToken);
				tokenRepository.Save(existTokenInfo?.Id, tokenInfo);
			}
			else
			{
				tokenRepository.Save(tokenInfo);
			}
		}

		private static List<Claim> BuildClaims(BaseAuthUser userEntity)
		{
			List<Claim> claims = new List<Claim>();

			switch (userEntity.UserType)
			{
				case "ADSecurity":
					claims.Add(new Claim("ADSecurity", "True"));
					claims.Add(new Claim("UserType", "ADSecurity"));
					break;
				case "SAML":
					claims.Add(new Claim("UserType", "SAML"));
					break;
				case "WX":
					claims.Add(new Claim("Id", userEntity.Id.ToString()));
					claims.Add(new Claim("Name", userEntity.UserName.ToString()));
					claims.Add(new Claim("UserType", "WX"));
					break;
				default:
					claims.Add(new Claim("Id", userEntity.Id.ToString()));
					claims.Add(new Claim("UserType", "TFSecurity"));
					break;
			}

			return claims;
		}

		private static DateTime GetExpiration()
		{
			return DateTime.Now.AddMinutes(GetExpirationMinutes());
		}

		private static string GetClaimValueFromToken(string token, string claimKey)
		{
			if (string.IsNullOrEmpty(token))
			{
				return null;
			}

			try
			{
				// Since the token should have been validated at the begining of the Request pipeline, we can use ReadToken (without signature validation) to save descryption cost
				var claimsDict = GetAllClaims(token).ToDictionary(c => c.Type, c => c.Value);

				if (claimsDict.ContainsKey(claimKey))
				{
					return claimsDict[claimKey];
				}
				else
				{
					return null;
				}
			}
			catch
			{
				return null;
			}
		}

		private static TokenInfo FindTokenInfo(string tokenKey, string token, TokenInfoRepository tokenRepository)
		{
			TokenInfo tokenValue = null;
			var retryCount = 0;
			while (retryCount < 3)
			{
				try
				{
					tokenValue = tokenRepository.FindTokenInfoByKey(tokenKey);
					break;
				}
				catch (Exception)
				{
					return null;
					Thread.Sleep(TokenRetryPauseInSeconds * 1000);
					retryCount++;
				}
			}

			return tokenValue;
		}

		private static IEnumerable<Claim> ValidateLoginToken(string token)
		{
			var configuration = ServiceLocator.Resolve<IConfiguration>();
			var httpContext = ServiceLocator.Resolve<IHttpContextAccessor>();
			var tokenHandler = new JwtSecurityTokenHandler();
			var issuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(configuration["Jwt:Secret"]));
			var validationParameters = new TokenValidationParameters()
			{
				ValidAudience = configuration["Jwt:Audience"],
				IssuerSigningKey = issuerSigningKey,
				ValidIssuer = configuration["Jwt:Issuer"],
				RequireExpirationTime = false,
				ValidateLifetime = false,
				ValidateAudience = false,
				ValidateIssuer = false,
				ValidateIssuerSigningKey = true,
			};
			try
			{
				var principal = tokenHandler.ValidateToken(token, validationParameters, out SecurityToken securityToken);
				var claims = principal.Claims.ToDictionary(c => c.Type, d => d.Value);

				if (!claims.ContainsKey("Name"))
				{
					return null;
				}

				var tokenKey = GetTokenKey(claims["Name"], claims["Prefix"]);
				var tokenRepository = new TokenInfoRepository(ServiceLocator.Resolve<ApiDBContent>(), ServiceLocator.Resolve<IDaemonDistributedCache>());
				var isSupport = SupportModeHelper.IsSupportModeUser(claims["Name"]);
				var preTokenValue = FindTokenInfo(tokenKey, token, tokenRepository);
				if (preTokenValue == null || !string.Equals(preTokenValue.TokenValue, token))
				{
					// force getting TokenInfo from database instead of cache with explicit token value
					var databaseToken = tokenRepository.FindTokenInfoByKeyAndValue(tokenKey, token);
					if (databaseToken != null)
					{
						// database token matched request, cache is out of sync so force backfill via save
						// this can happen due to Redis replication lag or user signing in during outage
						tokenRepository.Save(databaseToken);
					}
					else
					{
						return null;
					}
				}

				if (preTokenValue.Expiration > DateTime.Now)
				{
					var headers = httpContext.HttpContext.Request?.Headers;
					if (headers == null
						|| !headers.TryGetValue("no-extend-expiration", out StringValues noExtendExpirationValues)
						|| !noExtendExpirationValues.Any()
						|| !bool.TryParse(noExtendExpirationValues.FirstOrDefault(), out bool noExtendExpiration)
						|| !noExtendExpiration)
					{
						try
						{
							// refresh the expiration for current token.
							preTokenValue.Expiration = GetExpiration();
							SaveToken(isSupport, preTokenValue);
						}
						catch (Exception)
						{
						}
					}

					return principal.Claims;
				}

				ServiceLocator.Resolve<SessionItemRepository>().DeleteUserSession(Convert.ToInt32(claims["UserId"] ?? "-1"), LogoutReasonEnum.SessionTimeout);

				if (isSupport)
				{
					tokenRepository.Delete(tokenKey, token);
				}
				else
				{
					tokenRepository.Delete(tokenKey);
				}

				return null;
			}
			catch (Exception ex)
			{
				return null;
			}
		}

		internal static string GetTokenKey(string userName, string prefix)
		{
			return $"{prefix}_{userName}";
		}

		internal static IEnumerable<Claim> ValidateToken(string token)
		{
			if (string.IsNullOrEmpty(token))
			{
				var requestUrl = ServiceLocator.Resolve<IHttpContextAccessor>().HttpContext?.Request?.Path;
				var urlInfo = requestUrl != null ? requestUrl.ToString() : " from core service";
				return null;
			}

			try
			{
				var claims = GetAllClaims(token);
				return ValidateLoginToken(token);
			}
			catch (Exception)
			{
				return null;
			}
		}

		internal static int GetExpirationMinutes()
		{
			var context = ServiceLocator.Resolve<ApiDBContent>();
			int? sessionTimeout = context.ClientConfig.FirstOrDefault()?.SessionTimeout;
			return sessionTimeout.Value;
		}

		internal static IEnumerable<Claim> GetAllClaims(string token)
		{
			try
			{
				var tokenHandler = new JwtSecurityTokenHandler();
				var jwtToken = (JwtSecurityToken)tokenHandler.ReadToken(token);
				return jwtToken.Claims;
			}
			catch
			{
				return new List<Claim>();
			}
		}
	}
}